<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Webcam Filter Playground</title>
  <style>
    :root {
      --bg: #0b0d13;
      --panel: #111524;
      --text: #e6e7ea;
      --muted: #9aa0ad;
      --accent: #6aa7ff;
      --accent-2: #00e5a8;
      --danger: #ff5571;
      --radius: 14px;
      --shadow: 0 10px 30px rgba(0,0,0,.35), 0 2px 8px rgba(0,0,0,.25);
    }
    * { box-sizing: border-box; }
    html, body { height: 100%; }
    body {
      margin: 0;
      background: radial-gradient(1200px 800px at 80% -10%, #152040 0%, transparent 55%),
                  radial-gradient(900px 700px at -10% 90%, #0f192e 0%, transparent 60%),
                  var(--bg);
      color: var(--text);
      font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
      -webkit-font-smoothing: antialiased;
    }
    .wrap {
      max-width: 980px;
      margin: 0 auto;
      padding: 20px clamp(12px, 3vw, 28px) 28px;
      display: grid;
      gap: 14px;
      justify-items: center;
    }
    header {
      display: grid;
      gap: 6px;
      justify-items: center;
      text-align: center;
    }
    h1 { font-size: clamp(20px, 3.6vw, 28px); margin: 0; letter-spacing: 0.3px; }
    .sub { color: var(--muted); font-size: 13px; }
    .stage {
      width: min(100%, 960px);
      background: linear-gradient(180deg, #0e1425, #0b0f1d);
      border: 1px solid rgba(255,255,255,.06);
      border-radius: var(--radius);
      box-shadow: var(--shadow);
      padding: 12px;
      display: grid;
      gap: 10px;
      justify-items: center;
    }
    canvas#view {
      width: 100%;
      height: auto;
      border-radius: 10px;
      background: #000;
      aspect-ratio: 16/9;
    }
    video { display: none; }
    .toolbar {
      width: 100%;
      display: grid;
      grid-template-columns: 1fr;
      gap: 10px;
    }
    .controls {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      align-items: center;
      justify-content: center;
    }
    .select, .range, .buttons { display: flex; gap: 10px; align-items: center; }
    select, button, input[type="range"] {
      appearance: none;
      -webkit-appearance: none;
      background: var(--panel);
      color: var(--text);
      border: 1px solid rgba(255,255,255,.08);
      border-radius: 10px;
      padding: 10px 12px;
    }
    select { min-width: 210px; }
    input[type="range"] {
      height: 36px;
      width: min(360px, 70vw);
      padding: 0 6px;
    }
    input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; border-radius: 50%; background: linear-gradient(135deg, var(--accent), var(--accent-2)); border: none; box-shadow: 0 2px 10px rgba(0,0,0,.5); }
    input[type="range"]::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: linear-gradient(135deg, var(--accent), var(--accent-2)); border: none; box-shadow: 0 2px 10px rgba(0,0,0,.5); }
    button { cursor: pointer; border: 1px solid rgba(255,255,255,.12); font-weight: 600; letter-spacing: .2px; }
    button.primary { background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #071017; border: none; }
    button:disabled { opacity: .6; cursor: not-allowed; }
    .meta { display: flex; gap: 10px; justify-content: center; color: var(--muted); font-size: 12px; }
    .gallery {
      width: 100%;
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      margin-top: 6px;
      justify-content: center;
    }
    .thumb {
      height: 92px;
      aspect-ratio: 16/9;
      border-radius: 8px;
      border: 1px solid rgba(255,255,255,.08);
      box-shadow: 0 4px 20px rgba(0,0,0,.25);
      object-fit: cover;
      background: #0c0f18;
    }
    .hint { color: var(--muted); font-size: 12px; text-align: center; }
    .warn { color: #ffd166; }
  </style>
</head>
<body>
  <div class="wrap">
    <header>
      <h1>Webcam Filter Playground</h1>
      <div class="sub">Apply fun and surprising effects to your live webcam, capture, and save.</div>
    </header>
    <section class="stage">
      <video id="video" playsinline autoplay muted></video>
      <canvas id="view"></canvas>
      <div class="toolbar">
        <div class="controls">
          <div class="select">
            <label for="filter">Effect</label>
            <select id="filter" title="Choose a filter"></select>
          </div>
          <div class="range">
            <label for="intensity">Amount</label>
            <input id="intensity" type="range" min="0" max="100" value="60" />
          </div>
          <div class="buttons">
            <button id="capture" class="primary">Capture</button>
            <button id="save" title="Save last capture" disabled>Save</button>
          </div>
        </div>
        <div class="meta">
          <div id="status">Initializing camera…</div>
          <div id="fps"></div>
        </div>
      </div>
    </section>
    <div id="gallery" class="gallery"></div>
    <div class="hint">Tip: Press C to capture, S to save. Camera access requires a secure context (https or localhost). No data leaves your device.</div>
  </div>

  <script>
    const video = document.getElementById('video');
    const canvas = document.getElementById('view');
    const ctxOut = canvas.getContext('2d', { willReadFrequently: true });
    const statusEl = document.getElementById('status');
    const fpsEl = document.getElementById('fps');
    const filterSel = document.getElementById('filter');
    const intensityEl = document.getElementById('intensity');
    const captureBtn = document.getElementById('capture');
    const saveBtn = document.getElementById('save');
    const gallery = document.getElementById('gallery');

    let workCanvas, workCtx, tinyCanvas, tinyCtx;
    let w = 1280, h = 720;
    let running = true;
    let lastSnapURL = null;
    let lastFpsT = performance.now(), frameCount = 0;

    const clamp = (v, a, b) => v < a ? a : (v > b ? b : v);
    const lerp = (a, b, t) => a + (b - a) * t;

    const Filters = {
      none: {
        name: 'None',
        render: ({ srcCanvas, outCtx }) => {
          outCtx.drawImage(srcCanvas, 0, 0);
        }
      },
      grayscale: {
        name: 'Grayscale',
        render: ({ srcCtx, outCtx, intensity }) => {
          const img = srcCtx.getImageData(0, 0, w, h);
          const d = img.data;
          const t = intensity;
          for (let i = 0; i < d.length; i += 4) {
            const r = d[i], g = d[i+1], b = d[i+2];
            const l = 0.2126*r + 0.7152*g + 0.0722*b;
            d[i]   = lerp(r, l, t);
            d[i+1] = lerp(g, l, t);
            d[i+2] = lerp(b, l, t);
          }
          outCtx.putImageData(img, 0, 0);
        }
      },
      sepia: {
        name: 'Sepia',
        render: ({ srcCtx, outCtx, intensity }) => {
          const img = srcCtx.getImageData(0, 0, w, h);
          const d = img.data; const t = intensity;
          for (let i = 0; i < d.length; i += 4) {
            const r = d[i], g = d[i+1], b = d[i+2];
            const sr = clamp(0.393*r + 0.769*g + 0.189*b, 0, 255);
            const sg = clamp(0.349*r + 0.686*g + 0.168*b, 0, 255);
            const sb = clamp(0.272*r + 0.534*g + 0.131*b, 0, 255);
            d[i]   = lerp(r, sr, t);
            d[i+1] = lerp(g, sg, t);
            d[i+2] = lerp(b, sb, t);
          }
          outCtx.putImageData(img, 0, 0);
        }
      },
      invert: {
        name: 'Invert',
        render: ({ srcCtx, outCtx, intensity }) => {
          const img = srcCtx.getImageData(0, 0, w, h);
          const d = img.data; const t = intensity;
          for (let i = 0; i < d.length; i += 4) {
            const r = d[i], g = d[i+1], b = d[i+2];
            d[i]   = lerp(r, 255 - r, t);
            d[i+1] = lerp(g, 255 - g, t);
            d[i+2] = lerp(b, 255 - b, t);
          }
          outCtx.putImageData(img, 0, 0);
        }
      },
      posterize: {
        name: 'Posterize',
        render: ({ srcCtx, outCtx, intensity }) => {
          const img = srcCtx.getImageData(0, 0, w, h);
          const d = img.data; const levels = Math.max(2, Math.round(lerp(4, 16, intensity)));
          const step = 255 / (levels - 1);
          for (let i = 0; i < d.length; i += 4) {
            d[i]   = Math.round(d[i]   / step) * step;
            d[i+1] = Math.round(d[i+1] / step) * step;
            d[i+2] = Math.round(d[i+2] / step) * step;
          }
          outCtx.putImageData(img, 0, 0);
        }
      },
      pixelate: {
        name: 'Pixelate',
        render: ({ srcCanvas, outCtx, intensity }) => {
          const scale = Math.round(lerp(4, 32, intensity));
          const tw = Math.max(1, Math.floor(w / scale));
          const th = Math.max(1, Math.floor(h / scale));
          if (!tinyCanvas) { tinyCanvas = document.createElement('canvas'); tinyCtx = tinyCanvas.getContext('2d'); }
          tinyCanvas.width = tw; tinyCanvas.height = th;
          tinyCtx.imageSmoothingEnabled = false;
          tinyCtx.drawImage(srcCanvas, 0, 0, tw, th);
          outCtx.imageSmoothingEnabled = false;
          outCtx.clearRect(0, 0, w, h);
          outCtx.drawImage(tinyCanvas, 0, 0, w, h);
          outCtx.imageSmoothingEnabled = true;
        }
      },
      rgb_split: {
        name: 'RGB Split',
        render: ({ srcCtx, outCtx, intensity }) => {
          const src = srcCtx.getImageData(0, 0, w, h);
          const sd = src.data; const dst = outCtx.createImageData(w, h); const dd = dst.data;
          const off = Math.max(1, Math.round(lerp(2, 18, intensity)));
          for (let y = 0; y < h; y++) {
            for (let x = 0; x < w; x++) {
              const i = (y*w + x) * 4;
              const xr = clamp(x - off, 0, w - 1), xg = clamp(x + off, 0, w - 1);
              const yb = clamp(y - off, 0, h - 1);
              const ir = (y*w + xr) * 4;
              const ig = (y*w + xg) * 4;
              const ib = (yb*w + x) * 4;
              dd[i]   = sd[ir];
              dd[i+1] = sd[ig+1];
              dd[i+2] = sd[ib+2];
              dd[i+3] = 255;
            }
          }
          outCtx.putImageData(dst, 0, 0);
        }
      },
      edge: {
        name: 'Edge Detect',
        render: ({ srcCtx, outCtx, intensity }) => {
          const img = srcCtx.getImageData(0, 0, w, h);
          const d = img.data;
          const gray = new Float32Array(w*h);
          for (let p = 0, j = 0; p < d.length; p += 4, j++) {
            gray[j] = 0.2126*d[p] + 0.7152*d[p+1] + 0.0722*d[p+2];
          }
          const out = outCtx.createImageData(w, h);
          const od = out.data;
          for (let y = 1; y < h - 1; y++) {
            for (let x = 1; x < w - 1; x++) {
              const i = y*w + x;
              const gx = -gray[i-w-1] - 2*gray[i-1] - gray[i+w-1] + gray[i-w+1] + 2*gray[i+1] + gray[i+w+1];
              const gy = -gray[i-w-1] - 2*gray[i-w] - gray[i-w+1] + gray[i+w-1] + 2*gray[i+w] + gray[i+w+1];
              const mag = clamp(Math.abs(gx) + Math.abs(gy), 0, 255);
              const k = i * 4;
              od[k] = od[k+1] = od[k+2] = mag;
              od[k+3] = 255;
            }
          }
          const blend = intensity;
          for (let p = 0; p < d.length; p += 4) {
            od[p]   = lerp(d[p],   od[p],   blend);
            od[p+1] = lerp(d[p+1], od[p+1], blend);
            od[p+2] = lerp(d[p+2], od[p+2], blend);
          }
          outCtx.putImageData(out, 0, 0);
        }
      },
      thermal: {
        name: 'Thermal Cam',
        render: ({ srcCtx, outCtx, intensity }) => {
          const img = srcCtx.getImageData(0, 0, w, h);
          const d = img.data; const t = intensity;
          for (let i = 0; i < d.length; i += 4) {
            const l = (0.2126*d[i] + 0.7152*d[i+1] + 0.0722*d[i+2]) / 255;
            const n = clamp(l, 0, 1);
            const r = Math.floor(255 * clamp(Math.sin((n + t*0.15) * Math.PI), 0, 1));
            const g = Math.floor(255 * clamp(Math.sin((n + 0.33 + t*0.1) * Math.PI), 0, 1));
            const b = Math.floor(255 * clamp(Math.sin((n + 0.66 + t*0.05) * Math.PI), 0, 1));
            d[i] = r; d[i+1] = g; d[i+2] = b;
          }
          outCtx.putImageData(img, 0, 0);
        }
      },
      mirror4: {
        name: 'Kaleidoscope (4-way)',
        render: ({ srcCtx, outCtx }) => {
          const src = srcCtx.getImageData(0, 0, w, h);
          const sd = src.data; const dst = outCtx.createImageData(w, h); const dd = dst.data;
          const hw = Math.floor(w/2), hh = Math.floor(h/2);
          for (let y = 0; y < h; y++) {
            const sy = y < hh ? y : (h - 1 - y);
            for (let x = 0; x < w; x++) {
              const sx = x < hw ? x : (w - 1 - x);
              const si = (sy*w + sx) * 4;
              const di = (y*w + x) * 4;
              dd[di] = sd[si]; dd[di+1] = sd[si+1]; dd[di+2] = sd[si+2]; dd[di+3] = 255;
            }
          }
          outCtx.putImageData(dst, 0, 0);
        }
      },
      wavy: {
        name: 'Wavy Warp',
        render: ({ srcCtx, outCtx, time, intensity }) => {
          const src = srcCtx.getImageData(0, 0, w, h);
          const sd = src.data; const dst = outCtx.createImageData(w, h); const dd = dst.data;
          const amp = lerp(1, 14, intensity);
          const t = time * 0.0025;
          for (let y = 0; y < h; y++) {
            const syOff = Math.sin((y * 0.06) + t) * amp;
            for (let x = 0; x < w; x++) {
              const sxOff = Math.sin((x * 0.05) - t * 1.35) * amp;
              const sx = clamp(Math.round(x + sxOff), 0, w - 1);
              const sy = clamp(Math.round(y + syOff), 0, h - 1);
              const si = (sy*w + sx) * 4;
              const di = (y*w + x) * 4;
              dd[di] = sd[si]; dd[di+1] = sd[si+1]; dd[di+2] = sd[si+2]; dd[di+3] = 255;
            }
          }
          outCtx.putImageData(dst, 0, 0);
        }
      },
    };

    const filterOrder = ['none','grayscale','sepia','invert','posterize','pixelate','rgb_split','edge','thermal','mirror4','wavy'];

    function populateFilters() {
      filterOrder.forEach(id => {
        const opt = document.createElement('option');
        opt.value = id; opt.textContent = Filters[id].name;
        filterSel.appendChild(opt);
      });
      filterSel.value = 'rgb_split';
    }

    async function initCam() {
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false });
        video.srcObject = stream;
        await video.play();
        await new Promise(r => { if (video.readyState >= 1) r(); else video.onloadedmetadata = r; });
        w = video.videoWidth || 1280;
        h = video.videoHeight || 720;
        canvas.width = w; canvas.height = h;
        workCanvas = document.createElement('canvas');
        workCanvas.width = w; workCanvas.height = h;
        workCtx = workCanvas.getContext('2d', { willReadFrequently: true });
        statusEl.textContent = 'Camera ready';
        running = true;
        requestAnimationFrame(tick);
      } catch (e) {
        statusEl.innerHTML = '<span class="warn">Camera access failed.</span> Open this page over https or localhost and allow camera permissions.';
      }
    }

    function currentIntensity() { return parseInt(intensityEl.value, 10) / 100; }

    function tick(t) {
      if (!running) return;
      if (video.readyState >= 2) {
        workCtx.drawImage(video, 0, 0, w, h);
        const id = filterSel.value;
        const f = Filters[id];
        f.render({ srcCtx: workCtx, srcCanvas: workCanvas, outCtx: ctxOut, width: w, height: h, time: t, intensity: currentIntensity() });
      }
      frameCount++;
      const dt = t - lastFpsT;
      if (dt > 500) {
        const fps = Math.round((frameCount / dt) * 1000);
        fpsEl.textContent = fps + ' fps';
        frameCount = 0; lastFpsT = t;
      }
      requestAnimationFrame(tick);
    }

    function capture() {
      try { if (lastSnapURL) URL.revokeObjectURL(lastSnapURL); } catch {}
      const dataURL = canvas.toDataURL('image/png');
      const bin = atob(dataURL.split(',')[1]);
      const arr = new Uint8Array(bin.length);
      for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
      const file = new Blob([arr], { type: 'image/png' });
      lastSnapURL = URL.createObjectURL(file);
      const a = document.createElement('a');
      const ts = new Date();
      const pad = n => String(n).padStart(2, '0');
      a.download = `webcam-filter-${ts.getFullYear()}-${pad(ts.getMonth()+1)}-${pad(ts.getDate())}_${pad(ts.getHours())}-${pad(ts.getMinutes())}-${pad(ts.getSeconds())}.png`;
      a.href = lastSnapURL;
      const img = document.createElement('img');
      img.src = lastSnapURL; img.className = 'thumb'; img.alt = 'Snapshot';
      a.appendChild(img);
      gallery.prepend(a);
      saveBtn.disabled = false;
      statusEl.textContent = 'Snapshot captured';
      return a;
    }

    function saveLast() {
      if (!lastSnapURL) return;
      const a = gallery.querySelector('a');
      if (a) a.click();
    }

    captureBtn.addEventListener('click', capture);
    saveBtn.addEventListener('click', saveLast);
    window.addEventListener('keydown', (e) => {
      if (e.key.toLowerCase() === 'c') capture();
      if (e.key.toLowerCase() === 's') saveLast();
    });

    populateFilters();
    initCam();
  </script>
</body>
</html>
